<?php
/**
 * WPThemeReview Coding Standard.
 *
 * @package WPTRT\WPThemeReview
 * @link    https://github.com/WPTRT/WPThemeReview
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WPThemeReview\Sniffs\Plugins;

use WordPressCS\WordPress\Sniff;
use PHP_CodeSniffer\Util\Tokens;

/**
 * WordPress_Sniffs_Theme_CorrectTGMPAVersionSniff.
 *
 * Verifies that if the TGM Plugin Activation library is included, the correct version is used.
 * - Check whether the version included is up to date.
 * - Check whether the version included is downloaded via the Custom Generator with
 *   the correct settings.
 * - Check against a persistent manual search & replace error made by theme authors.
 *
 * @link  https://make.wordpress.org/themes/handbook/review/...... @todo
 *
 * @since 0.1.0
 *
 * {@internal This sniff currently has not (yet) been detached from the WordPressCS\WordPress\Sniff class
 * as the `detect_manual_editing()` method uses the WPCS `get_function_call_parameter()` and
 * `strip_quotes()` methods.
 * If/when WPCS re-organizes the generic methods into traits as is being discussed in
 * {@link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues/1465},
 * this sniff could be decoupled and use the trait(s) instead.}}
 */
class CorrectTGMPAVersionSniff extends Sniff {

	const GITHUB_TGMPA_API_URL = 'https://api.github.com/repos/TGMPA/TGM-Plugin-Activation/releases/latest';

	const GITHUB_API_OAUTH_QUERY = '?access_token=%s';

	/**
	 * TGMPA flavour required.
	 *
	 * By default, any flavour is ok. If this property is set in a custom ruleset an additional
	 * check will be run to verify if the correct flavour of TGMPA is used.
	 *
	 * Note: The Custom Generator only has flavours for themes, so this property is only
	 * relevant when checking a theme.
	 *
	 * Valid values:
	 * - 'wporg'
	 * - 'themeforest'
	 *
	 * @var string
	 */
	public $required_flavour = '';

	/**
	 * GitHub oAuth token.
	 *
	 * Intended to be set in the ruleset.
	 *
	 * This is to prevent issues with rate limiting if a lot of requests are made from the same server.
	 *
	 * "Normal" users generaly won't need to set this, but if the sniffs are run for all themes
	 * uploaded to wordpress.org, that IP address might run into the rate limit of 60 calls per hour.
	 * Setting a oauth token in the custom ruleset used will prevent this.
	 *
	 * Alternatively, the token can also be set via an environment key called `GITHUB_OAUTH_TOKEN`.
	 *
	 * @var string
	 */
	public $github_oauth_token = '';

	/**
	 * Fall-back for the latest version number for if the API call fails.
	 *
	 * @var string
	 */
	private $current_version = '2.6.1';

	/**
	 * Whether or not a call has been made to the GitHub API to retrieve the current TGMPA version number.
	 *
	 * Note: an API call will only be made once per PHPCS run and only when the TGMPA library
	 * has been positively identified.
	 * The API call will be skipped when running the unit tests.
	 *
	 * @var bool
	 */
	private $gh_call_made = false;

	/**
	 * Classes and functions declared by TGMPA.
	 *
	 * @var array
	 */
	private $tgmpa_classes_functions = array(
		// Classes.
		'TGM_Plugin_Activation'      => true,
		'TGMPA_List_Table'           => true,
		'TGM_Bulk_Installer'         => true,
		'TGM_Bulk_Installer_Skin'    => true,
		'TGMPA_Bulk_Installer'       => true,
		'TGMPA_Bulk_Installer_Skin'  => true,
		'TGMPA_Utils'                => true,
		// Functions.
		'load_tgm_plugin_activation' => true,
		'tgmpa_initialize'           => true, // New in v 2.6.2.
		'tgmpa'                      => true,
		'tgmpa_load_bulk_installer'  => true,
	);

	/**
	 * List of available TGMPA flavours - other than the default.
	 *
	 * Used to validate `$required_flavour` property.
	 *
	 * Keys are the allowed values for the flavour property.
	 * Value is how this is expanded in the @version tag used by TGMPA.
	 *
	 * @var array
	 */
	private $valid_flavours = array(
		'wporg'       => 'WordPress.org',
		'themeforest' => 'ThemeForest',
	);

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		return array(
			T_OPEN_TAG,
		);
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @param int $stackPtr The position of the current token in the stack.
	 *
	 * @return int|void Integer stack pointer to skip forward or void to continue
	 *                  normal file processing.
	 */
	public function process_token( $stackPtr ) {

		$has_class_function = $this->phpcsFile->findNext( array( T_CLASS, T_FUNCTION ), ( $stackPtr + 1 ) );
		if ( false === $has_class_function ) {
			// No class, function or constant declaration found, definitely not TGMPA file.
			// Skip this file from further checks.
			return ( $this->phpcsFile->numTokens + 1 );
		}

		$is_tgmpa = false;

		// First check based on filename.
		$file_name = basename( $this->phpcsFile->getFileName() );
		if ( false !== $file_name ) {
			$file_name = strtolower( $file_name );
			if ( strpos( $file_name, 'class-tgm-plugin-activation.php' ) !== false ) {
				$is_tgmpa = true;
			} elseif ( defined( 'PHP_CODESNIFFER_IN_TESTS' ) && preg_match( '`class-tgm-plugin-activation[^.]+\.inc`', $file_name, $discard ) > 0 ) {
				$is_tgmpa = true;
			}
		}

		/*
		 * Otherwise, check whether any of the TGMPA classes or function names are encountered.
		 *
		 * Will detect TGMPA, even when:
		 * - the class and function prefix has been changed
		 * - the file has been split up into several files
		 * - the file is combined with other code
		 */
		if ( false === $is_tgmpa ) {
			while ( false !== $has_class_function ) {
				$name = $this->phpcsFile->getDeclarationName( $has_class_function );
				if ( ! empty( $name ) ) {
					if ( isset( $this->tgmpa_classes_functions[ $name ] ) ) {
						$is_tgmpa = true;
						break;
					} elseif ( strpos( $name, '_Plugin_Activation' ) !== false ) {
						// This may be TGMPA with renamed prefixes, so look for typical class comment.
						$prev = $this->phpcsFile->findPrevious( T_WHITESPACE, ( $has_class_function - 1 ), null, true );
						while ( false !== $prev && isset( Tokens::$commentTokens[ $this->tokens[ $prev ]['code'] ] ) ) {
							if ( ( T_COMMENT === $this->tokens[ $prev ]['code']
								|| T_DOC_COMMENT_STRING === $this->tokens[ $prev ]['code'] )
								&& ( strpos( $this->tokens[ $prev ]['content'], 'Automatic plugin installation and activation library' ) !== false
								|| strpos( $this->tokens[ $prev ]['content'], 'Automatic plugin installation and activation class' ) !== false )
							) {
								$is_tgmpa = true;
								break;
							}

							$prev = $this->phpcsFile->findPrevious( T_WHITESPACE, ( $prev - 1 ), null, true );
						}
					}
				}

				$start = ( $has_class_function + 1 );
				if ( isset( $this->tokens[ $has_class_function ]['scope_condition'], $this->tokens[ $stackPtr ]['scope_closer'] )
					&& $this->tokens[ $has_class_function ]['scope_condition'] === $has_class_function
				) {
					// Skip past anything within the class or function.
					$start = $this->tokens[ $stackPtr ]['scope_closer'];
				}

				$has_class_function = $this->phpcsFile->findNext( array( T_CLASS, T_FUNCTION ), $start );
			}
		}

		// If we're still not 100% sure this is TGMPA, exclude the file from further checks.
		if ( false === $is_tgmpa ) {
			return ( $this->phpcsFile->numTokens + 1 );
		}

		// Check whether the correct version of TGMPA is being used.
		$version = $this->uses_latest_version();

		// Check for typical manual search & replace error.
		$this->detect_manual_editing( $version );

		// No need to check the same file again.
		return ( $this->phpcsFile->numTokens + 1 );
	}

	/**
	 * Check whether the latest version of TGMPA is being used.
	 */
	protected function uses_latest_version() {
		if ( false === $this->gh_call_made ) {
			// Get the current version number for TGMPA from GitHub.
			$this->update_current_version();
			$this->gh_call_made = true;
		}

		/*
		 * Walk the doc block comments to find if this is the correct version of TGMPA.
		 * Normally this will be in the first doc block encountered, so this is not as 'heavy' as it looks.
		 */
		$next_doc_block    = 0;
		$version           = false;
		$pcre_esc_flavours = array_map(
			'preg_quote',
			$this->valid_flavours,
			array_fill( 0, count( $this->valid_flavours ), '`' )
		);
		$pcre_flavours     = implode( '|', $pcre_esc_flavours );

		do {
			$next_doc_block = $this->phpcsFile->findNext( T_DOC_COMMENT_OPEN_TAG, ( $next_doc_block + 1 ) );

			if ( false === $next_doc_block ) {
				break;
			}

			$tags = $this->get_docblock_tags( $next_doc_block );
			if ( empty( $tags ) ) {
				continue;
			}

			if ( ! isset( $tags['package'], $tags['version'] ) || 'TGM-Plugin-Activation' !== $tags['package'] ) {
				continue;
			}

			if ( preg_match( '`^([0-9\.]+(?:-(?:alpha|beta|RC)(?:[0-9])?)?)`', $tags['version'], $matches ) > 0 ) {

				$version = $matches[1];
				$this->phpcsFile->recordMetric( 0, 'Version', $version );

				if ( true === version_compare( $this->current_version, $version, '>' ) ) {
					$error = 'Upgrade of the included TGM plugin activation library required. Current version: %s. Found version: %s';
					$data  = array(
						$this->current_version,
						$version,
					);
					$this->phpcsFile->addError( $error, 0, 'upgradeRequired', $data );

				} elseif ( true === version_compare( $this->current_version, $version, '<' ) ) {
					$error = 'Non-stable version of the TGM plugin activation library found. The current version is %s. Found version: %s';
					$data  = array(
						$this->current_version,
						$version,
					);
					$this->phpcsFile->addError( $error, 0, 'unstableVersion', $data );
				}
				unset( $matches, $error, $data );

				if ( strpos( $tags['version'], 'for parent theme' ) !== false ) {
					$this->phpcsFile->recordMetric( 0, 'Used in', 'parent theme' );
				} elseif ( strpos( $tags['version'], 'for child theme' ) !== false ) {
					$this->phpcsFile->recordMetric( 0, 'Used in', 'child theme' );
				} else {
					$this->phpcsFile->recordMetric( 0, 'Used in', 'unknown' );
				}

				if ( preg_match( '`for publication on (' . $pcre_flavours . ')`i', $tags['version'], $flavour_match ) > 0 ) {
					$this->phpcsFile->recordMetric( 0, 'Publication Channel', $flavour_match[1] );
				} else {
					$this->phpcsFile->recordMetric( 0, 'Publication Channel', 'n/a' );
				}

				if ( ! empty( $this->required_flavour ) && isset( $this->valid_flavours[ $this->required_flavour ] ) ) {
					$flavour_phrase = sprintf( 'for publication on %s', $this->valid_flavours[ $this->required_flavour ] );
					if ( strpos( $tags['version'], $flavour_phrase ) === false ) {
						$warning = 'You are required to use a version of the TGM Plugin Activation library downloaded through the Custom TGMPA Generator. Download a fresh copy and make sure you select "%s" as your publication channel to get the correct version. http://tgmpluginactivation.com/download/';
						$this->phpcsFile->addWarning(
							$warning,
							0,
							'wrongVersion',
							array( $this->valid_flavours[ $this->required_flavour ] )
						);
					}
				}
			}
			break;

		} while ( false !== $next_doc_block );

		// The file was recognized as TGMPA, but no valid file doc block for TGMPA was found.
		if ( false === $version ) {

			$this->phpcsFile->recordMetric( 0, 'Version', 'unknown' );

			$error = 'The TGM Plugin Activation library was detected, but the version could not be determined. Ensure you use the latest stable release of the TGM Plugin Activation library (%s). Download a fresh copy now using the Custom TGMPA Generator at http://tgmpluginactivation.com/download/';
			$data  = array( $this->current_version );
			$this->phpcsFile->addError( $error, 0, 'versionUndetermined', $data );
			$has_error = true;
		}

		return $version;
	}

	/**
	 * Check against a typical manual search & replace error often encountered.
	 *
	 * In that case the `tgmpa` in `if ( ! function_exists( 'tgmpa' ) )` has been replaced
	 * with the theme slug causing fatal errors when an end-user also uses a plugin using TGMPA.
	 *
	 * @param string $version The version of TGMPA found.
	 */
	protected function detect_manual_editing( $version ) {
		// Skip this check for TGMPA versions which didn't have the `tgmpa()` function or
		// didn't have the `function_exists()` wrapper.
		if ( false === $version || true === version_compare( $version, '2.2.0', '<' ) ) {
			return;
		}

		$checkTokens = array(
			// This is what we're looking for.
			T_FUNCTION         => true,
			// These are just here to be able to skip as much as we can.
			T_CLASS            => true,
			T_ARRAY            => true,
			T_OPEN_SHORT_ARRAY => true,
		);

		for ( $ptr = 0; $ptr < $this->phpcsFile->numTokens; $ptr++ ) {
			if ( ! isset( $checkTokens[ $this->tokens[ $ptr ]['code'] ] ) ) {
				continue;
			}

			// Skip as much as we can.
			if ( T_CLASS === $this->tokens[ $ptr ]['code'] && isset( $this->tokens[ $ptr ]['scope_closer'] ) ) {
				$ptr = $this->tokens[ $ptr ]['scope_closer'];
				continue;
			} elseif ( T_OPEN_SHORT_ARRAY === $this->tokens[ $ptr ]['code'] && isset( $this->tokens[ $ptr ]['bracket_closer'] ) ) {
				$ptr = $this->tokens[ $ptr ]['bracket_closer'];
				continue;
			} elseif ( T_ARRAY === $this->tokens[ $ptr ]['code'] && isset( $this->tokens[ $ptr ]['parenthesis_closer'] ) ) {
				$ptr = $this->tokens[ $ptr ]['parenthesis_closer'];
				continue;
			}

			// Detect whether this is the `tgmpa()` function declaration.
			if ( T_FUNCTION === $this->tokens[ $ptr ]['code'] ) {
				$function_name = $this->phpcsFile->getDeclarationName( $ptr );
				if ( 'tgmpa' !== $function_name ) {
					if ( isset( $this->tokens[ $ptr ]['scope_closer'] ) ) {
						// Skip the rest of the function.
						$ptr = $this->tokens[ $ptr ]['scope_closer'];
					}
					continue;
				}

				// Ok, found the tgmpa function declaration. Now let's check for the typical
				// manual text-domain replacement error.
				$function_exists = $this->phpcsFile->findPrevious( T_STRING, ( $ptr - 1 ), null, false, 'function_exists' );
				$param           = false;
				if ( false !== $function_exists ) {
					$param = $this->get_function_call_parameter( $function_exists, 1 );
					$param = $this->strip_quotes( $param['raw'] );
				}

				if ( false === $function_exists || 'tgmpa' !== $param ) {
					$this->phpcsFile->recordMetric( 0, 'Manual editing detected', 'yes' );
					$this->phpcsFile->addError(
						'Manual editing of the TGM Plugin Activation file detected. Your edit will cause fatal errors for end-users. Download an official copy using the Custom TGMPA Generator. http://tgmpluginactivation.com/download/',
						0,
						'ManualEditDetected',
						array(),
						9
					);
				} else {
					$this->phpcsFile->recordMetric( 0, 'Manual editing detected', 'no' );
				}
				break;
			}
		}
	}

	/**
	 * Retrieve an array with the doc block tags from a T_DOC_COMMENT_OPEN_TAG.
	 *
	 * @param int $comment_opener The position of the comment opener.
	 *
	 * @return array
	 */
	protected function get_docblock_tags( $comment_opener ) {
		$tags   = array();
		$opener = $this->tokens[ $comment_opener ];

		if ( ! isset( $opener['comment_tags'] ) ) {
			return $tags;
		}

		$closer = null;
		if ( isset( $opener['comment_closer'] ) ) {
			$closer = $opener['comment_closer'];
		}

		$tag_count = count( $opener['comment_tags'] );

		for ( $i = 0; $i < $tag_count; $i++ ) {
			$tag_token = $opener['comment_tags'][ $i ];
			$tag       = trim( $this->tokens[ $tag_token ]['content'], '@' );

			$search_end = $closer;
			if ( ( $i + 1 ) < $tag_count ) {
				$search_end = $opener['comment_tags'][ ( $i + 1 ) ];
			}

			$value_token  = $this->phpcsFile->findNext( T_DOC_COMMENT_STRING, ( $tag_token + 1 ), $search_end );
			$tags[ $tag ] = trim( $this->tokens[ $value_token ]['content'] );
			unset( $tag_token, $tag, $search_end, $value );
		}

		return $tags;
	}

	/**
	 * Get the version number (tag_name) of the latest TGMPA release from the GitHub API.
	 */
	protected function update_current_version() {
		if ( defined( 'PHP_CODESNIFFER_IN_TESTS' ) || true === $this->gh_call_made ) {
			return;
		}

		$api_url     = self::GITHUB_TGMPA_API_URL;
		$oauth_token = false;
		if ( '' !== $this->github_oauth_token && is_string( $this->github_oauth_token ) ) {
			$oauth_token = $this->github_oauth_token;
		} elseif ( false !== getenv( 'GITHUB_OAUTH_TOKEN' ) ) {
			$oauth_token = getenv( 'GITHUB_OAUTH_TOKEN' );
		}

		if ( false !== $oauth_token ) {
			$api_url .= sprintf( self::GITHUB_API_OAUTH_QUERY, $oauth_token );
		}

		$stream_options = array(
			'http' => array(
				'method'           => 'GET',
				'user_agent'       => 'WordPress-Coding-Standards/Theme-Review-Sniffs',
				'protocol_version' => 1.1,
			),
		);
		$stream_context = stream_context_create( $stream_options );
		$response       = file_get_contents( $api_url, false, $stream_context );
		$headers        = $this->parse_response_headers( $http_response_header );

		// Check for invalid oAuth token response.
		if ( 401 === $headers['response_code'] && false !== $oauth_token ) {
			$this->phpcsFile->addWarning(
				'The GITHUB_OAUTH_TOKEN you provided is invalid. Please update the token in your custom ruleset or environment properties.',
				0,
				'githubOauthTokenInvalid'
			);
			$this->oauth_error = false;
			return;
		}

		// Check for rate limit error response.
		if ( 403 === $headers['response_code'] && '0' === $headers['X-RateLimit-Remaining'] ) {
			// @todo Add link to GH wiki page documenting the properties.
			$this->phpcsFile->addWarning(
				'You are running PHPCS more than 60 times per hour. You may want to consider setting the `github_oauth_token` property in your custom ruleset for Theme Review. For more information see: ... (GH wiki page).',
				0,
				'githubRateLimitReached'
			);
			$this->rate_limit_error = false;
			return;
		}

		if ( 200 !== $headers['response_code'] ) {
			// Something unexpected going on, just ignore it.
			return;
		}

		// Ok, we have received a valid response.
		$response = json_decode( $response );
		if ( ! empty( $response->tag_name ) && ( ! isset( $response->prerelease ) || false === $response->prerelease ) ) {
			// Should there be a check for `v` at the start of a version number ?
			$this->current_version = $response->tag_name;
		}
	}

	/**
	 * Parse HTTP response headers array to a more usable format.
	 *
	 * Based on http://php.net/manual/en/reserved.variables.httpresponseheader.php#117203
	 *
	 * @param array $headers HTTP response headers array.
	 *
	 * @return array
	 */
	private function parse_response_headers( $headers ) {
		$head = array();
		foreach ( $headers as $key => $value ) {
			$tag = explode( ':', $value, 2 );
			if ( isset( $tag[1] ) ) {
				$head[ trim( $tag[0] ) ] = trim( $tag[1] );
			} else {
				$head[] = $value;
				if ( preg_match( '`HTTP/[0-9\.]+\s+([0-9]+)`', $value, $out ) ) {
					$head['response_code'] = intval( $out[1] );
				}
			}
		}
		return $head;
	}

}


<?php
/**
 * WPThemeReview Coding Standard.
 *
 * @package WPTRT\WPThemeReview
 * @link    https://github.com/WPTRT/WPThemeReview
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WPThemeReview\Sniffs\CoreFunctionality;

use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;

/**
 * Check if a theme uses include(_once) or require(_once) when get_template_part() should be used.
 *
 * @link  https://make.wordpress.org/themes/handbook/review/required/#core-functionality-and-features
 *
 * @since 0.1.0
 */
class FileIncludeSniff implements Sniff {

	/**
	 * A list of files to skip.
	 *
	 * @var array
	 */
	protected $file_whitelist = array(
		'functions.php' => true,
	);

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		return Tokens::$includeTokens;
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
	 *                                               token was found.
	 * @param int                         $stackPtr  The position of the current token
	 *                                               in the stack.
	 *
	 * @return void
	 */
	public function process( File $phpcsFile, $stackPtr ) {

		$tokens    = $phpcsFile->getTokens();
		$token     = $tokens[ $stackPtr ];
		$file_name = basename( $phpcsFile->getFileName() );

		if ( ! isset( $this->file_whitelist[ $file_name ] ) ) {
			$phpcsFile->addWarning(
				'Check that %s is not being used to load template files. "get_template_part()" should be used to load template files.',
				$stackPtr,
				'FileIncludeFound',
				array( $token['content'] )
			);
		}
	}

}


<?php
/**
 * WPThemeReview Coding Standard.
 *
 * @package WPTRT\WPThemeReview
 * @link    https://github.com/WPTRT/WPThemeReview
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WPThemeReview\Sniffs\CoreFunctionality;

use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;

/**
 * Restricts the use of the <title> tag, unless it is within a <svg> tag.
 *
 * @link  https://make.wordpress.org/themes/handbook/review/required/
 *
 * @since 0.1.0
 */
class NoTitleTagSniff implements Sniff {

	/**
	 * Property to keep track of whether a <svg> open tag has been encountered.
	 *
	 * @var array
	 */
	private $in_svg;

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		return Tokens::$textStringTokens;
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
	 *                                               token was found.
	 * @param int                         $stackPtr  The position of the current token
	 *                                               in the stack.
	 *
	 * @return void
	 */
	public function process( File $phpcsFile, $stackPtr ) {

		$tokens   = $phpcsFile->getTokens();
		$content  = $tokens[ $stackPtr ]['content'];
		$filename = $phpcsFile->getFileName();

		// Set to false if it is the first time this sniff is run on a file.
		if ( ! isset( $this->in_svg[ $filename ] ) ) {
			$this->in_svg[ $filename ] = false;
		}

		// No need to check an empty string.
		if ( '' === trim( $content ) ) {
			return;
		}

		// Are we in a <svg> tag ?
		if ( true === $this->in_svg[ $filename ] ) {
			if ( false === strpos( $content, '</svg>' ) ) {
				return;
			} else {
				// Make sure we check any content on this line after the closing svg tag.
				$this->in_svg[ $filename ] = false;
				$content                   = trim( substr( $content, ( strpos( $content, '</svg>' ) + 6 ) ) );
			}
		}

		// We're not in svg, so check if it there's a <svg> open tag on this line.
		if ( false !== strpos( $content, '<svg' ) ) {
			if ( false === strpos( $content, '</svg>' ) ) {
				// Skip the next lines until the closing svg tag, but do check any content
				// on this line before the svg tag.
				$this->in_svg[ $filename ] = true;
				$content                   = trim( substr( $content, 0, ( strpos( $content, '<svg' ) ) ) );
			} else {
				// Ok, we have open and close svg tag on the same line with possibly content before and/or after.
				$before  = trim( substr( $content, 0, ( strpos( $content, '<svg' ) ) ) );
				$after   = trim( substr( $content, ( strpos( $content, '</svg>' ) + 6 ) ) );
				$content = $before . $after;
			}
		}

		// Now let's do the check for the <title> tag.
		if ( false !== strpos( $content, '<title' ) ) {
			$phpcsFile->addError(
				"The title tag must not be used. Use add_theme_support( 'title-tag' ) instead.",
				$stackPtr,
				'TagFound'
			);
		}
	}

}


<?php
/**
 * WPThemeReview Coding Standard.
 *
 * @package WPTRT\WPThemeReview
 * @link    https://github.com/WPTRT/WPThemeReview
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WPThemeReview\Sniffs\CoreFunctionality;

use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;

/**
 * Check for hardcoded favicons instead of using core implementation.
 *
 * @link  https://make.wordpress.org/themes/handbook/review/required/#core-functionality-and-features
 *
 * @since 0.1.0
 */
class NoFaviconSniff implements Sniff {

	/**
	 * Regex template.
	 *
	 * Will be parsed together with the attribute blacklist in the register() method.
	 *
	 * @var string
	 */
	const REGEX_TEMPLATE = '` (?:%s)`i';

	/**
	 * (Partial) Regex template for recognizing a HTML attribute.
	 *
	 * Will be parsed together with the attribute blacklist in the register() method.
	 *
	 * @var string
	 */
	const REGEX_ATTR_TEMPLATE = '%1$s=[\'"](?:%2$s)[\'"]';

	/**
	 * List of link and meta attributes that are blacklisted.
	 *
	 * @var array
	 */
	protected $attribute_blacklist = array(
		'rel' => array(
			'icon',
			'shortcut icon',
			'bookmark icon',
			'apple-touch-icon',
			'apple-touch-icon-precomposed',
		),
		'name' => array(
			'msapplication-config',
			'msapplication-TileImage',
			'msapplication-square70x70logo',
			'msapplication-square150x150logo',
			'msapplication-wide310x150logo',
			'msapplication-square310x310logo',
		),
	);

	/**
	 * The regex to catch the blacklisted attributes.
	 *
	 * @var string
	 */
	protected $favicon_regex;

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		// Build the regex to be used only once.
		$regex_parts = array();

		foreach ( $this->attribute_blacklist as $key => $values ) {
			$values        = array_map( 'preg_quote', $values, array_fill( 0, count( $values ), '`' ) );
			$values        = implode( '|', $values );
			$regex_parts[] = sprintf( self::REGEX_ATTR_TEMPLATE, preg_quote( $key, '`' ), $values );
		}

		$this->favicon_regex = sprintf( self::REGEX_TEMPLATE, implode( '|', $regex_parts ) );

		return Tokens::$textStringTokens;
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
	 *                                               token was found.
	 * @param int                         $stackPtr  The position of the current token
	 *                                               in the stack.
	 *
	 * @return void
	 */
	public function process( File $phpcsFile, $stackPtr ) {

		$tokens = $phpcsFile->getTokens();
		$token  = $tokens[ $stackPtr ];

		if ( preg_match( $this->favicon_regex, $token['content'] ) > 0 ) {
			$phpcsFile->addError(
				'Code for favicon found. Favicons are handled by the "Site Icon" setting in the customizer since WP version 4.3.',
				$stackPtr,
				'NoFavicon'
			);
		}
	}

}


<?php
/**
 * WPThemeReview Coding Standard.
 *
 * @package WPTRT\WPThemeReview
 * @link    https://github.com/WPTRT/WPThemeReview
 * @license https://opensource.org/licenses/MIT MIT
 */

namespace WPThemeReview\Sniffs\PluginTerritory;

use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;

/**
 * Discourages the use of the session variable.
 * Creating a session writes a file to the server and is unreliable in a multi-server environment.
 *
 * @link  https://make.wordpress.org/themes/handbook/review/...... @todo
 *
 * @since WPCS 0.3.0
 * @since WPCS 0.10.0 The sniff no longer needlessly extends the Generic_Sniffs_PHP_ForbiddenFunctionsSniff
 *                    which it didn't use.
 * @since WPCS 0.12.0 This class now extends WordPress_Sniff.
 * @since WPCS 0.13.0 Class name changed: this class is now namespaced.
 *
 * @since TRTCS 0.1.0 As this sniff will be removed from WPCS in version 2.0, the
 *                    sniff has been cherry-picked into the WPThemeReview standard.
 */
class SessionVariableUsageSniff implements Sniff {

	/**
	 * Returns an array of tokens this test wants to listen for.
	 *
	 * @return array
	 */
	public function register() {
		return array(
			\T_VARIABLE,
		);
	}

	/**
	 * Processes this test, when one of its tokens is encountered.
	 *
	 * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
	 *                                               token was found.
	 * @param int                         $stackPtr  The position of the current token
	 *                                               in the stack.
	 *
	 * @return void
	 */
	public function process( File $phpcsFile, $stackPtr ) {

		$tokens = $phpcsFile->getTokens();

		if ( '$_SESSION' === $tokens[ $stackPtr ]['content'] ) {
			$phpcsFile->addError(
				'Usage of $_SESSION variable is prohibited.',
				$stackPtr,
				'SessionVarsProhibited'
			);
		}
	}

}


<?php
namespace Base32;

/**
 * Base32 encoder and decoder
 *
 * Last update: 2012-06-20
 *
 * RFC 4648 compliant
 * @link http://www.ietf.org/rfc/rfc4648.txt
 *
 * Some groundwork based on this class
 * https://github.com/NTICompass/PHP-Base32
 *
 * @author Christian Riesen <chris.riesen@gmail.com>
 * @link http://christianriesen.com
 * @license MIT License see LICENSE file
 */
class Base32
{
    /**
     * Alphabet for encoding and decoding base32
     *
     * @var array
     */
    private static $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=';

    /**
     * Creates an array from a binary string into a given chunk size
     *
     * @param string $binaryString String to chunk
     * @param integer $bits Number of bits per chunk
     * @return array
     */
    private static function chunk($binaryString, $bits)
    {
        $binaryString = chunk_split($binaryString, $bits, ' ');

        if (substr($binaryString, (strlen($binaryString)) - 1)  == ' ') {
            $binaryString = substr($binaryString, 0, strlen($binaryString)-1);
        }

        return explode(' ', $binaryString);
    }

    /**
     * Encodes into base32
     *
     * @param string $string Clear text string
     * @return string Base32 encoded string
     */
    public static function encode($string)
    {
        if (strlen($string) == 0) {
            // Gives an empty string

            return '';
        }

        // Convert string to binary
        $binaryString = '';

        foreach (str_split($string) as $s) {
            // Return each character as an 8-bit binary string
            $binaryString .= sprintf('%08b', ord($s));
        }

        // Break into 5-bit chunks, then break that into an array
        $binaryArray = self::chunk($binaryString, 5);

        // Pad array to be divisible by 8
        while (count($binaryArray) % 8 !== 0) {
            $binaryArray[] = null;
        }

        $base32String = '';

        // Encode in base32
        foreach ($binaryArray as $bin) {
            $char = 32;

            if (!is_null($bin)) {
                // Pad the binary strings
                $bin = str_pad($bin, 5, 0, STR_PAD_RIGHT);
                $char = bindec($bin);
            }

            // Base32 character
            $base32String .= self::$alphabet[$char];
        }

        return $base32String;
    }

    /**
     * Decodes base32
     *
     * @param string $base32String Base32 encoded string
     * @return string Clear text string
     */
    public static function decode($base32String)
    {
        // Only work in upper cases
        $base32String = strtoupper($base32String);

        // Remove anything that is not base32 alphabet
        $pattern = '/[^A-Z2-7]/';

        $base32String = preg_replace($pattern, '', $base32String);

        if (strlen($base32String) == 0) {
            // Gives an empty string
            return '';
        }

        $base32Array = str_split($base32String);

        $string = '';

        foreach ($base32Array as $str) {
            $char = strpos(self::$alphabet, $str);

            // Ignore the padding character
            if ($char !== 32) {
                $string .= sprintf('%05b', $char);
            }
        }

        while (strlen($string) %8 !== 0) {
            $string = substr($string, 0, strlen($string)-1);
        }

        $binaryArray = self::chunk($string, 8);

        $realString = '';

        foreach ($binaryArray as $bin) {
            // Pad each value to 8 bits
            $bin = str_pad($bin, 8, 0, STR_PAD_RIGHT);
            // Convert binary strings to ASCII
            $realString .= chr(bindec($bin));
        }

        return $realString;
    }
}


<?php

namespace Amsgames\LaravelShop\Traits;

/**
 * This file is part of LaravelShop,
 * A shop solution for Laravel.
 *
 * @author Alejandro Mostajo
 * @copyright Amsgames, LLC
 * @license MIT
 * @package Amsgames\LaravelShop
 */

use Shop;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;

trait ShopCalculationsTrait
{
    /**
     * Property used to stored calculations.
     * @var array
     */
    private $shopCalculations = null;

    /**
     * Returns total amount of items in cart.
     *
     * @return int
     */
    public function getCountAttribute()
    {
        if (empty($this->shopCalculations)) $this->runCalculations();
        return round($this->shopCalculations->itemCount, 2);
    }

    /**
     * Returns total price of all the items in cart.
     *
     * @return float
     */
    public function getTotalPriceAttribute()
    {
        if (empty($this->shopCalculations)) $this->runCalculations();
        return round($this->shopCalculations->totalPrice, 2);
    }

    /**
     * Returns total tax of all the items in cart.
     *
     * @return float
     */
    public function getTotalTaxAttribute()
    {
        if (empty($this->shopCalculations)) $this->runCalculations();
        return round($this->shopCalculations->totalTax + ($this->totalPrice * Config::get('shop.tax')), 2);
    }

    /**
     * Returns total tax of all the items in cart.
     *
     * @return float
     */
    public function getTotalShippingAttribute()
    {
        if (empty($this->shopCalculations)) $this->runCalculations();
        return round($this->shopCalculations->totalShipping, 2);
    }

    /**
     * Returns total discount amount based on all coupons applied.
     *
     * @return float
     */
    public function getTotalDiscountAttribute() { /* TODO */ }

    /**
     * Returns total amount to be charged base on total price, tax and discount.
     *
     * @return float
     */
    public function getTotalAttribute()
    {
        if (empty($this->shopCalculations)) $this->runCalculations();
        return $this->totalPrice + $this->totalTax + $this->totalShipping;
    }

    /**
     * Returns formatted total price of all the items in cart.
     *
     * @return string
     */
    public function getDisplayTotalPriceAttribute()
    {
        return Shop::format($this->totalPrice);
    }

    /**
     * Returns formatted total tax of all the items in cart.
     *
     * @return string
     */
    public function getDisplayTotalTaxAttribute()
    {
        return Shop::format($this->totalTax);
    }

    /**
     * Returns formatted total tax of all the items in cart.
     *
     * @return string
     */
    public function getDisplayTotalShippingAttribute()
    {
        return Shop::format($this->totalShipping);
    }

    /**
     * Returns formatted total discount amount based on all coupons applied.
     *
     * @return string
     */
    public function getDisplayTotalDiscountAttribute() { /* TODO */ }

    /**
     * Returns formatted total amount to be charged base on total price, tax and discount.
     *
     * @return string
     */
    public function getDisplayTotalAttribute()
    {
        return Shop::format($this->total);
    }

    /**
     * Returns cache key used to store calculations.
     *
     * @return string.
     */
    public function getCalculationsCacheKeyAttribute()
    {
        return 'shop_' . $this->table . '_' . $this->attributes['id'] . '_calculations';
    }

    /**
     * Runs calculations.
     */
    private function runCalculations()
    {
        if (!empty($this->shopCalculations)) return $this->shopCalculations;
        $cacheKey = $this->calculationsCacheKey;
        if (Config::get('shop.cache_calculations')
            && Cache::has($cacheKey)
        ) {
            $this->shopCalculations = Cache::get($cacheKey);
            return $this->shopCalculations;
        }
        $this->shopCalculations = DB::table($this->table)
            ->select([
                DB::raw('sum(' . Config::get('shop.item_table') . '.quantity) as itemCount'),
                DB::raw('sum(' . Config::get('shop.item_table') . '.price * ' . Config::get('shop.item_table') . '.quantity) as totalPrice'),
                DB::raw('sum(' . Config::get('shop.item_table') . '.tax * ' . Config::get('shop.item_table') . '.quantity) as totalTax'),
                DB::raw('sum(' . Config::get('shop.item_table') . '.shipping * ' . Config::get('shop.item_table') . '.quantity) as totalShipping')
            ])
            ->join(
                Config::get('shop.item_table'),
                Config::get('shop.item_table') . '.' . ($this->table == Config::get('shop.order_table') ? 'order_id' : $this->table . '_id'),
                '=',
                $this->table . '.id'
            )
            ->where($this->table . '.id', $this->attributes['id'])
            ->first();
        if (Config::get('shop.cache_calculations')) {
            Cache::put(
                $cacheKey,
                $this->shopCalculations,
                Config::get('shop.cache_calculations_minutes')
            );
        }
        return $this->shopCalculations;
    }

    /**
     * Resets cart calculations.
     */
    private function resetCalculations ()
    {
        $this->shopCalculations = null;
        if (Config::get('shop.cache_calculations')) {
            Cache::forget($this->calculationsCacheKey);
        }
    }

}


<?php

namespace Amsgames\LaravelShop\Core;

/**
 * This file is part of LaravelShop,
 * A shop solution for Laravel.
 *
 * @author Alejandro Mostajo
 * @copyright Amsgames, LLC
 * @license MIT
 * @package Amsgames\LaravelShop
 */

use JsonSerializable;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use Amsgames\LaravelShop\Contracts\PaymentGatewayInterface;

abstract class PaymentGateway implements PaymentGatewayInterface, Arrayable, Jsonable, JsonSerializable
{
    /**
     * Gateway Id, set by shop.
     *
     * @var mixed
     */
    protected $id;

    /**
     * Gateway generated transaction id.
     *
     * @var mixed
     */
    protected $transactionId;

    /**
     * Gateway generated transaction id.
     *
     * @var string
     */
    protected $detail = null;

    /**
     * Gateway generated token.
     *
     * @var mixed
     */
    protected $token = null;

    /**
     * Success call back url.
     *
     * @var mixed
     */
    protected $callbackSuccess = '';

    /**
     * Fail call back url.
     *
     * @var mixed
     */
    protected $callbackFail = '';

    /**
     * Status code after placing order successfully.
     *
     * @var string
     */
    protected $statusCode = 'completed';

    /**
     * Constructor.
     *
     * @param mixed $if Gateway id.
     */
    public function __construct($id = '')
    {
        $this->id            = $id;
        $this->transactionId = uniqid();
        $this->token         = uniqid();
    }

    /**
     * Called on cart checkout.
     *
     * @param Order $order Order.
     */
    public function onCheckout($cart)
    {
    }

    /**
     * Called by shop when payment gateway calls callback url.
     * Success result
     *
     * @param Order $order Order.
     * @param mixed $data  Callback data.
     *
     * @return string
     */
    public function onCallbackSuccess($order, $data = null)
    {
    }

    /**
     * Called by shop when payment gateway calls callback url.
     * Failed result
     *
     * @param Order $order Order.
     * @param mixed $data  Callback data.
     *
     * @return string
     */
    public function onCallbackFail($order, $data = null)
    {
    }

    /**
     * Sets callback urls
     *
     * @param Order $order Order.
     */
    public function setCallbacks($order)
    {
        $this->callbackSuccess = route(config('shop.callback_route'), [
            'status'    => 'success',
            'id'        => $order->id,
            'token'     => $this->token,
        ]);

        $this->callbackFail    = route(config('shop.callback_route'), [
            'status'    => 'fail',
            'id'        => $order->id,
            'token'     => $this->token,
        ]);
    }

    /**
     * Returns transaction token.
     *
     * @return mixed.
     */
    public function getTransactionToken()
    {
        return $this->token;
    }

    /**
     * Returns the transaction ID generated by the gateway.
     * i.e. PayPal's transaction ID.
     *
     * @return mixed
     */
    public function getTransactionId()
    {
        return $this->transactionId;
    }

    /**
     * Returns a 1024 length string with extra detail of transaction.
     *
     * @return string
     */
    public function getTransactionDetail()
    {
        return $this->detail;
    }

    /**
     * Returns transaction status code.
     *
     * @return string
     */
    public function getTransactionStatusCode()
    {
        return $this->statusCode;
    }

    /**
     * Convert the model instance to an array.
     *
     * @return array
     */
    public function toArray()
    {
        return ['id' => $this->id];
    }

    /**
     * Convert the model instance to JSON.
     *
     * @param  int  $options
     * @return string
     */
    public function toJson($options = 0)
    {
        return json_encode($this->jsonSerialize(), $options);
    }

    /**
     * Convert the object into something JSON serializable.
     *
     * @return array
     */
    public function jsonSerialize()
    {
        return $this->toArray();
    }

    /**
     * Convert the model to its string representation.
     *
     * @return string
     */
    public function __toString()
    {
        return $this->toJson();
    }

}


<?php

namespace Amsgames\LaravelShop;

/**
 * This file is part of LaravelShop,
 * A shop solution for Laravel.
 *
 * @author Alejandro Mostajo
 * @copyright Amsgames, LLC
 * @license MIT
 * @package Amsgames\LaravelShop
 */

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;

class MigrationCommand extends Command
{
    /**
     * The console command name.
     *
     * @var string
     */
    protected $name = 'laravel-shop:migration';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Creates a migration following the LaravelShop specifications.';

    /**
     * Execute the console command.
     *
     * @return void
     */
    public function fire()
    {
        $this->laravel->view->addNamespace('laravel-shop', substr(__DIR__, 0, -8).'views');

        $cartTable          = Config::get('shop.cart_table');
        $itemTable          = Config::get('shop.item_table');
        $couponTable        = Config::get('shop.coupon_table');
        $orderStatusTable   = Config::get('shop.order_status_table');
        $orderTable         = Config::get('shop.order_table');
        $transactionTable   = Config::get('shop.transaction_table');

        // Migrations
        $this->line('');
        $this->info( "Tables: $cartTable, $itemTable" );

        $message = "A migration that creates '$cartTable', '$itemTable', '$orderTable'".
        " tables will be created in database/migrations directory";

        $this->comment($message);
        $this->line('');

        if ($this->confirm('Proceed with the migration creation? [Yes|no]', 'Yes')) {

            $this->line('');

            $this->info('Creating migration...');
            if ($this->createMigration(compact(
                    'cartTable',
                    'itemTable',
                    'couponTable',
                    'orderStatusTable',
                    'orderTable',
                    'transactionTable'
                ))
            ) {

                $this->info('Migration successfully created!');
            } else {
                $this->error(
                    "Couldn't create migration.\n Check the write permissions".
                    " within the database/migrations directory."
                );
            }

        }

        // Seeder

        $this->line('');
        $this->info( "Table seeders: $orderStatusTable" );
        $message = "A seeder that seeds '$orderStatusTable' table(s) with data. Will be created in database/seeds directory";

        $this->comment($message);
        $this->line('');

        if ($this->confirm('Proceed with the seeder creation? [Yes|no]', 'Yes')) {

            $this->line('');

            $this->info('Creating seeder...');
            if ($this->createSeeder(compact(
                    'cartTable',
                    'itemTable',
                    'couponTable',
                    'orderStatusTable',
                    'orderTable',
                    'transactionTable'
                ))
            ) {
                $this->info('Seeder successfully created!');
            } else {
                $this->error(
                    "Couldn't create seeder.\n Check the write permissions".
                    " within the database/seeds directory."
                );
            }

        }
    }

    /**
     * Create the migration.
     *
     * @param array $data Data with table names.
     *
     * @return bool
     */
    protected function createMigration($data)
    {
        $migrationFile = base_path('/database/migrations') . '/' . date('Y_m_d_His') . '_shop_setup_tables.php';

        $usersTable  = Config::get('auth.table');
        $userModel   = Config::get('auth.model');
        $userKeyName = (new $userModel())->getKeyName();

        $data = array_merge($data, compact('usersTable', 'userKeyName'));

        $output = $this->laravel->view->make('laravel-shop::generators.migration')->with($data)->render();

        if (!file_exists($migrationFile) && $fs = fopen($migrationFile, 'x')) {
            fwrite($fs, $output);
            fclose($fs);
            return true;
        }

        return false;
    }

    /**
     * Create the seeder.
     *
     * @param array $data Data with table names.
     *
     * @return bool
     */
    protected function createSeeder($data)
    {
        $seederFile = base_path('/database/seeds') . '/LaravelShopSeeder.php';

        $output = $this->laravel->view->make('laravel-shop::generators.seeder')->with($data)->render();

        if (!file_exists($seederFile) && $fs = fopen($seederFile, 'x')) {
            fwrite($fs, $output);
            fclose($fs);
            return true;
        }

        return false;
    }
}


<?php

namespace Amsgames\LaravelShop\Traits;

/**
 * This file is part of LaravelShop,
 * A shop solution for Laravel.
 *
 * @author Alejandro Mostajo
 * @copyright Amsgames, LLC
 * @license MIT
 * @package Amsgames\LaravelShop
 */

use Auth;
use Shop;
use Illuminate\Support\Facades\Config;
use InvalidArgumentException;

trait ShopItemTrait
{

    /**
     * Returns flag indicating if item has an object.
     *
     * @return bool
     */
    public function getHasObjectAttribute()
    {
        return array_key_exists('class', $this->attributes) && !empty($this->attributes['class']);
    }

    /**
     * Returns flag indicating if the object is shoppable or not.
     *
     * @return bool
     */
    public function getIsShoppableAttribute()
    {
        return true;
    }

    /**
     * Returns attached object.
     *
     * @return mixed
     */
    public function getObjectAttribute()
    {
        return $this->hasObject ? call_user_func($this->attributes['class'] . '::find', $this->attributes['reference_id']) : null;
    }

    /**
     * Returns item name.
     *
     * @return string
     */
    public function getDisplayNameAttribute()
    {
        if ($this->hasObject) return $this->object->displayName;
        return isset($this->itemName)
            ? $this->attributes[$this->itemName]
            : (array_key_exists('name', $this->attributes)
                ? $this->attributes['name']
                : ''
            );
    }

    /**
     * Returns item id.
     *
     * @return mixed
     */
    public function getShopIdAttribute()
    {
        return is_array($this->primaryKey) ? 0 : $this->attributes[$this->primaryKey];
    }

    /**
     * Returns item url.
     *
     * @return string
     */
    public function getShopUrlAttribute()
    {
        if ($this->hasObject) return $this->object->shopUrl;
        if (!property_exists($this, 'itemRouteName') && !property_exists($this, 'itemRouteParams')) return '#';
        $params = [];
        foreach (array_keys($this->attributes) as $attribute) {
            if (in_array($attribute, $this->itemRouteParams)) $params[$attribute] = $this->attributes[$attribute];
        }
        return empty($this->itemRouteName) ? '#' : \route($this->itemRouteName, $params);
    }

    /**
     * Returns price formatted for display.
     *
     * @return string
     */
    public function getDisplayPriceAttribute()
    {
        return Shop::format($this->attributes['price']);
    }

    /**
     * Scope class by a given sku.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query  Query.
     * @param mixed                                 $sku    SKU.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeWhereSKU($query, $sku)
    {
        return $query->where('sku', $sku);
    }

    /**
     * Scope class by a given sku.
     * Returns item found.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query  Query.
     * @param mixed                                 $sku    SKU.
     *
     * @return this
     */
    public function scopeFindBySKU($query, $sku)
    {
        return $query->whereSKU($sku)->first();
    }

    /**
     * Returns formatted tax for display.
     *
     * @return string
     */
    public function getDisplayTaxAttribute()
    {
        return Shop::format(array_key_exists('tax', $this->attributes) ? $this->attributes['tax'] : 0.00);
    }

    /**
     * Returns formatted tax for display.
     *
     * @return string
     */
    public function getDisplayShippingAttribute()
    {
        return Shop::format(array_key_exists('shipping', $this->attributes) ? $this->attributes['shipping'] : 0.00);
    }

    /**
     * Returns flag indicating if item was purchased by user.
     *
     * @return bool
     */
    public function getWasPurchasedAttribute()
    {
        if (Auth::guest()) return false;
        return Auth::user()
            ->orders()
            ->whereSKU($this->attributes['sku'])
            ->whereStatusIn(config('shop.order_status_purchase'))
            ->count() > 0;
    }

}


<?php

namespace Amsgames\LaravelShop\Gateways;

/**
 * This file is part of LaravelShop,
 * A shop solution for Laravel.
 *
 * @author Alejandro Mostajo
 * @copyright Amsgames, LLC
 * @license MIT
 * @package Amsgames\LaravelShop
 */

use Amsgames\LaravelShop\Gateways\GatewayPass;

class GatewayCallback extends GatewayPass
{
    /**
     * For callback uses.
     */
    protected $didCallback = false;

    /**
     * Called by shop to charge order's amount.
     *
     * @param Order $order Order.
     *
     * @return bool
     */
    public function onCharge($order)
    {
        $this->statusCode   = 'pending';
        $this->detail       = 'pending response, token:' .  $this->token;
        return parent::onCharge($order);
    }

    /**
     * Called on callback.
     *
     * @param Order $order Order.
     * @param mixed $data  Request input from callback.
     *
     * @return bool
     */
    public function onCallbackSuccess($order, $data = null)
    {
        $this->statusCode   = 'completed';
        $this->detail       = 'success callback';
        $this->didCallback  = true;
    }

    /**
     * Called on callback.
     *
     * @param Order $order Order.
     * @param mixed $data  Request input from callback.
     *
     * @return bool
     */
    public function onCallbackFail($order, $data = null)
    {
        $this->statusCode   = 'failed';
        $this->detail       = 'failed callback';
        $this->didCallback  = true;
    }

    /**
     * Returns successful callback URL
     * @return false
     */
    public function getCallbackSuccess()
    {
        return $this->callbackSuccess;
    }

    /**
     * Returns fail callback URL
     * @return false
     */
    public function getCallbackFail()
    {
        return $this->callbackFail;
    }

    /**
     * Returns successful callback URL
     * @return false
     */
    public function getDidCallback()
    {
        return $this->didCallback;
    }
}


<?php

namespace Amsgames\LaravelShop\Traits;

/**
 * This file is part of LaravelShop,
 * A shop solution for Laravel.
 *
 * @author Alejandro Mostajo
 * @copyright Amsgames, LLC
 * @license MIT
 * @package Amsgames\LaravelShop
 */

use Shop;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;

trait ShopCartTrait
{
    /**
     * Property used to stored calculations.
     * @var array
     */
    private $cartCalculations = null;

    /**
     * Boot the user model
     * Attach event listener to remove the relationship records when trying to delete
     * Will NOT delete any records if the user model uses soft deletes.
     *
     * @return void|bool
     */
    public static function boot()
    {
        parent::boot();

        static::deleting(function($user) {
            if (!method_exists(Config::get('auth.providers.users.model'), 'bootSoftDeletingTrait')) {
                $user->items()->sync([]);
            }

            return true;
        });
    }

    /**
     * One-to-One relations with the user model.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function user()
    {
        return $this->belongsTo(config('auth.providers.users.model'), 'user_id');
    }

    /**
     * One-to-Many relations with Item.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function items()
    {
        return $this->hasMany(Config::get('shop.item'), 'cart_id');
    }

    /**
     * Adds item to cart.
     *
     * @param mixed $item     Item to add, can be an Store Item, a Model with ShopItemTrait or an array.
     * @param int   $quantity Item quantity in cart.
     */
    public function add($item, $quantity = 1, $quantityReset = false)
    {
        if (!is_array($item) && !$item->isShoppable) return;
        // Get item
        $cartItem = $this->getItem(is_array($item) ? $item['sku'] : $item->sku);
        // Add new or sum quantity
        if (empty($cartItem)) {
            $reflection = null;
            if (is_object($item)) {
                $reflection = new \ReflectionClass($item);
            }
            $cartItem = call_user_func( Config::get('shop.item') . '::create', [
                'user_id'       => $this->user->shopId,
                'cart_id'       => $this->attributes['id'],
                'sku'           => is_array($item) ? $item['sku'] : $item->sku,
                'price'         => is_array($item) ? $item['price'] : $item->price,
                'tax'           => is_array($item)
                                    ? (array_key_exists('tax', $item)
                                        ?   $item['tax']
                                        :   0
                                    )
                                    : (isset($item->tax) && !empty($item->tax)
                                        ?   $item->tax
                                        :   0
                                    ),
                'shipping'      => is_array($item)
                                    ? (array_key_exists('shipping', $item)
                                        ?   $item['shipping']
                                        :   0
                                    )
                                    : (isset($item->shipping) && !empty($item->shipping)
                                        ?   $item->shipping
                                        :   0
                                    ),
                'currency'      => Config::get('shop.currency'),
                'quantity'      => $quantity,
                'class'         => is_array($item) ? null : $reflection->getName(),
                'reference_id'  => is_array($item) ? null : $item->shopId,
            ]);
        } else {
            $cartItem->quantity = $quantityReset
                ? $quantity
                : $cartItem->quantity + $quantity;
            $cartItem->save();
        }
        $this->resetCalculations();
        return $this;
    }

    /**
     * Removes an item from the cart or decreases its quantity.
     * Returns flag indicating if removal was successful.
     *
     * @param mixed $item     Item to remove, can be an Store Item, a Model with ShopItemTrait or an array.
     * @param int   $quantity Item quantity to decrease. 0 if wanted item to be removed completly.
     *
     * @return bool
     */
    public function remove($item, $quantity = 0)
    {
        // Get item
        $cartItem = $this->getItem(is_array($item) ? $item['sku'] : $item->sku);
        // Remove or decrease quantity
        if (!empty($cartItem)) {
            if (!empty($quantity)) {
                $cartItem->quantity -= $quantity;
                $cartItem->save();
                if ($cartItem->quantity > 0) return true;
            }
            $cartItem->delete();
        }
        $this->resetCalculations();
        return $this;
    }

    /**
     * Checks if the user has a role by its name.
     *
     * @param string|array $name       Role name or array of role names.
     * @param bool         $requireAll All roles in the array are required.
     *
     * @return bool
     */
    public function hasItem($sku, $requireAll = false)
    {
        if (is_array($sku)) {
            foreach ($sku as $skuSingle) {
                $hasItem = $this->hasItem($skuSingle);

                if ($hasItem && !$requireAll) {
                    return true;
                } elseif (!$hasItem && $requireAll) {
                    return false;
                }
            }

            // If we've made it this far and $requireAll is FALSE, then NONE of the roles were found
            // If we've made it this far and $requireAll is TRUE, then ALL of the roles were found.
            // Return the value of $requireAll;
            return $requireAll;
        } else {
            foreach ($this->items as $item) {
                if ($item->sku == $sku) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Scope class by a given user ID.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query  Query.
     * @param mixed                                 $userId User ID.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeWhereUser($query, $userId)
    {
        return $query->where('user_id', $userId);
    }

    /**
     * Scope to current user cart.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query  Query.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeWhereCurrent($query)
    {
        if (Auth::guest()) return $query;
        return $query->whereUser(Auth::user()->shopId);
    }

    /**
     * Scope to current user cart and returns class model.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query  Query.
     *
     * @return this
     */
    public function scopeCurrent($query)
    {
        if (Auth::guest()) return;
        $cart = $query->whereCurrent()->first();
        if (empty($cart)) {
            $cart = call_user_func( Config::get('shop.cart') . '::create', [
                'user_id' =>  Auth::user()->shopId
            ]);
        }
        return $cart;
    }

    /**
     * Scope to current user cart and returns class model.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query  Query.
     *
     * @return this
     */
    public function scopeFindByUser($query, $userId)
    {
        if (empty($userId)) return;
        $cart = $query->whereUser($userId)->first();
        if (empty($cart)) {
            $cart = call_user_func( Config::get('shop.cart') . '::create', [
                'user_id' =>  $userId
            ]);
        }
        return $cart;
    }

    /**
     * Transforms cart into an order.
     * Returns created order.
     *
     * @param string $statusCode Order status to create order with.
     *
     * @return Order
     */
    public function placeOrder($statusCode = null)
    {
        if (empty($statusCode)) $statusCode = Config::get('shop.order_status_placement');
        // Create order
        $order = call_user_func( Config::get('shop.order') . '::create', [
            'user_id'       => $this->user_id,
            'statusCode'    => $statusCode
        ]);
        // Map cart items into order
        for ($i = count($this->items) - 1; $i >= 0; --$i) {
            // Attach to order
            $this->items[$i]->order_id  = $order->id;
            // Remove from cart
            $this->items[$i]->cart_id   = null;
            // Update
            $this->items[$i]->save();
        }
        $this->resetCalculations();
        return $order;
    }

    /**
     * Whipes put cart
     */
    public function clear()
    {
        DB::table(Config::get('shop.item_table'))
            ->where('cart_id', $this->attributes['id'])
            ->delete();
        $this->resetCalculations();
        return $this;
    }

    /**
     * Retrieves item from cart;
     *
     * @param string $sku SKU of item.
     *
     * @return mixed
     */
    private function getItem($sku)
    {
        $className  = Config::get('shop.item');
        $item       = new $className();
        return $item->where('sku', $sku)
            ->where('cart_id', $this->attributes['id'])
            ->first();
    }

}


<?php

namespace Amsgames\LaravelShop\Traits;

/**
 * This file is part of LaravelShop,
 * A shop solution for Laravel.
 *
 * @author Alejandro Mostajo
 * @copyright Amsgames, LLC
 * @license MIT
 * @package Amsgames\LaravelShop
 */

use Illuminate\Support\Facades\Config;
use InvalidArgumentException;

trait ShopTransactionTrait
{
    /**
     * One-to-One relations with the order model.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function order()
    {
        return $this->belongsTo(Config::get('shop.order_table'), 'order_id');
    }

    /**
     * Scopes to get transactions from user.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeWhereUser($query, $userId)
    {
        return $query->join(
                Config::get('shop.order_table'),
                Config::get('shop.order_table') . '.id',
                '=',
                Config::get('shop.transaction_table') . '.order_id'
            )
            ->where(Config::get('shop.order_table') . '.user_id', $userId);
    }
}


<?php

namespace Amsgames\LaravelShop;

/**
 * Service provider for laravel.
 *
 * @author Alejandro Mostajo
 * @copyright Amsgames, LLC
 * @license MIT
 * @package Amsgames\LaravelShop
 */

use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;

class LaravelShopProvider extends ServiceProvider
{

    /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
    protected $defer = false;

    /**
     * Bootstrap the application events.
     *
     * @return void
     */
    public function boot(Router $router)
    {
        parent::boot($router);

        // Publish config files
        $this->publishes([
            __DIR__ . '/Config/config.php' => config_path('shop.php'),
        ]);

        // Register commands
        $this->commands('command.laravel-shop.migration');

    }

    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->registerShop();

        $this->registerCommands();

        $this->mergeConfig();
    }

    /**
     * Register the application bindings.
     *
     * @return void
     */
    private function registerShop()
    {
        $this->app->singleton('shop', function ($app) {
            return new LaravelShop($app);
        });
    }

    /**
     * Merges user's and entrust's configs.
     *
     * @return void
     */
    private function mergeConfig()
    {
        $this->mergeConfigFrom(
            __DIR__ . '/Config/config.php', 'shop'
        );
    }

    /**
     * Register the artisan commands.
     *
     * @return void
     */
    private function registerCommands()
    {
        $this->app->singleton('command.laravel-shop.migration', function ($app) {
            return new MigrationCommand();
        });
    }

    /**
     * Get the services provided.
     *
     * @return array
     */
    public function provides()
    {
        return [
            'shop', 'command.laravel-shop.migration'
        ];
    }

    /**
     * Maps router.
     * Add package special controllers.
     *
     * @param Router $route Router.
     */
    public function map(Router $router)
    {
        $router->group(['namespace' => 'Amsgames\LaravelShop\Http\Controllers'], function($router) {

            $router->group(['prefix' => 'shop'], function ($router) {

                $router->get('callback/payment/{status}/{id}/{shoptoken}', ['as' => 'shop.callback', 'uses' => 'Shop\CallbackController@process']);

                $router->post('callback/payment/{status}/{id}/{shoptoken}', ['as' => 'shop.callback', 'uses' => 'Shop\CallbackController@process']);

            });

        });
    }

}


<?php

namespace Amsgames\LaravelShop\Traits;

/**
 * This file is part of LaravelShop,
 * A shop solution for Laravel.
 *
 * @author Alejandro Mostajo
 * @copyright Amsgames, LLC
 * @license MIT
 * @package Amsgames\LaravelShop
 */

use Illuminate\Support\Facades\Config;
use InvalidArgumentException;

trait ShopOrderTrait
{
    /**
     * Boot the user model
     * Attach event listener to remove the relationship records when trying to delete
     * Will NOT delete any records if the user model uses soft deletes.
     *
     * @return void|bool
     */
    public static function boot()
    {
        parent::boot();

        //static::deleting(function($user) {
           // if (!method_exists(Config::get('auth.model'), 'bootSoftDeletingTrait')) {
                //$user->items()->sync([]);
           // }

           // return true;
        //});
    }

    /**
     * One-to-One relations with the user model.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function user()
    {
        return $this->belongsTo(config('auth.providers.users.model'), 'user_id');
    }

    /**
     * One-to-Many relations with Item.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function items()
    {
        return $this->hasMany(Config::get('shop.item'), 'order_id');
    }

    /**
     * One-to-Many relations with Item.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function transactions()
    {
        return $this->hasMany(Config::get('shop.transaction'), 'order_id');
    }

    /**
     * Returns flag indicating if order is lock and cant be modified by the user.
     * An order is locked the moment it enters pending status.
     *
     * @return bool
     */
    public function getIsLockedAttribute() {
        return in_array($this->attributes['statusCode'], Config::get('shop.order_status_lock'));
    }

    /**
     * Scopes class by user ID and returns object.
     * Optionally, scopes by status.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query  Query.
     * @param mixed                                 $userId User ID.
     *
     * @return this
     */
    public function scopeWhereUser($query, $userId) {
        return $query->where('user_id', $userId);
    }

    /**
     * Scopes class by item sku.
     * Optionally, scopes by status.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query  Query.
     * @param mixed                                 $sku    Item SKU.
     *
     * @return this
     */
    public function scopeWhereSKU($query, $sku) {
        return $query->join(
                config('shop.item_table'),
                config('shop.item_table') . '.order_id',
                '=',
                $this->table . '.id'
            )
            ->where(config('shop.item_table') . '.sku', $sku);
    }

    /**
     * Scopes class by user ID and returns object.
     * Optionally, scopes by status.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query      Query.
     * @param string                                $statusCode Status.
     *
     * @return this
     */
    public function scopeWhereStatus($query, $statusCode) {
        return $query = $query->where('statusCode', $statusCode);
    }

    /**
     * Scopes class by status codes.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query       Query.
     * @param array                                 $statusCodes Status.
     *
     * @return this
     */
    public function scopeWhereStatusIn($query, array $statusCodes) {
        return $query = $query->whereIn('statusCode', $statusCodes);
    }

    /**
     * Scopes class by user ID and returns object.
     * Optionally, scopes by status.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query      Query.
     * @param mixed                                 $userId     User ID.
     * @param string                                $statusCode Status.
     *
     * @return this
     */
    public function scopeFindByUser($query, $userId, $statusCode = null) {
        if (!empty($status)) {
            $query = $query->whereStatus($status);
        }
        return $query->whereUser($userId)->get();
    }

    /**
     * Returns flag indicating if order is in the status specified.
     *
     * @param string $status Status code.
     *
     * @return bool
     */
    public function is($statusCode)
    {
        return $this->attributes['statusCode'] == $statusCode;
    }

    /**
     * Returns flag indicating if order is completed.
     *
     * @return bool
     */
    public function getIsCompletedAttribute()
    {
        return $this->attributes['statusCode'] == 'completed';
    }

    /**
     * Returns flag indicating if order has failed.
     *
     * @return bool
     */
    public function getHasFailedAttribute()
    {
        return $this->attributes['statusCode'] == 'failed';
    }

    /**
     * Returns flag indicating if order is canceled.
     *
     * @return bool
     */
    public function getIsCanceledAttribute()
    {
        return $this->attributes['statusCode'] == 'canceled';
    }

    /**
     * Returns flag indicating if order is in process.
     *
     * @return bool
     */
    public function getIsInProcessAttribute()
    {
        return $this->attributes['statusCode'] == 'in_process';
    }

    /**
     * Returns flag indicating if order is in creation.
     *
     * @return bool
     */
    public function getIsInCreationAttribute()
    {
        return $this->attributes['statusCode'] == 'in_creation';
    }

    /**
     * Returns flag indicating if order is in creation.
     *
     * @return bool
     */
    public function getIsPendingAttribute()
    {
        return $this->attributes['statusCode'] == 'pending';
    }

    /**
     * Creates the order's transaction.
     *
     * @param string $gateway       Gateway.
     * @param mixed  $transactionId Transaction ID.
     * @param string $detail        Transaction detail.
     *
     * @return object
     */
    public function placeTransaction($gateway, $transactionId, $detail = null, $token = null)
    {
        return call_user_func(Config::get('shop.transaction') . '::create', [
            'order_id'          => $this->attributes['id'],
            'gateway'           => $gateway,
            'transaction_id'    => $transactionId,
            'detail'            => $detail,
            'token'             => $token,
        ]);
    }

    /**
     * Retrieves item from order;
     *
     * @param string $sku SKU of item.
     *
     * @return mixed
     */
    private function getItem($sku)
    {
        $className  = Config::get('shop.item');
        $item       = new $className();
        return $item->where('sku', $sku)
            ->where('order_id', $this->attributes['id'])
            ->first();
    }
}


<?php
namespace Amsgames\LaravelShop\Http\Controllers\Shop;
use Validator;
use Shop;
use Illuminate\Http\Request;
use Amsgames\LaravelShop\Http\Controllers\Controller;
class CallbackController extends Controller
{
    /**
     * Process payment callback.
     *
     * @param Request $request   Request.
     *
     * @return redirect
     */
    protected function process(Request $request)
    {
        $validator = Validator::make(
            [
                'order_id'  => $request->get('order_id'),
                'status'    => $request->get('status'),
                'shoptoken' => $request->get('shoptoken'),
            ],
            [
                'order_id'  => 'required|exists:' . config('shop.order_table') . ',id',
                'status'    => 'required|in:success,fail',
                'shoptoken' => 'required|exists:' . config('shop.transaction_table') . ',token,order_id,' . $request->get('order_id'),
            ]
        );
        if ($validator->fails()) {
            abort(404);
        }
        $order = call_user_func(config('shop.order') . '::find', $request->get('order_id'));
        $transaction = $order->transactions()->where('token', $request->get('shoptoken'))->first();
        Shop::callback($order, $transaction, $request->get('status'), $request->all());
        $transaction->token = null;
        $transaction->save();
        return redirect()->route(config('shop.callback_redirect_route'), ['orderId' => $order->id]);
    }
}


<?php

namespace Amsgames\LaravelShop;

/**
 * This class is the main entry point of laravel shop. Usually this the interaction
 * with this class will be done through the LaravelShop Facade
 *
 * @author Alejandro Mostajo
 * @copyright Amsgames, LLC
 * @license MIT
 * @package Amsgames\LaravelShop
 */

use Auth;
use Amsgames\LaravelShop\Gateways;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Log;
use Amsgames\LaravelShop\Exceptions\CheckoutException;
use Amsgames\LaravelShop\Exceptions\GatewayException;
use Amsgames\LaravelShop\Exceptions\ShopException;
use Amsgames\LaravelShop\Events\CartCheckout;
use Amsgames\LaravelShop\Events\OrderCompleted;
use Amsgames\LaravelShop\Events\OrderPlaced;
use Amsgames\LaravelShop\Events\OrderStatusChanged;

class LaravelShop
{
    /**
     * Forces quantity to reset when adding items to cart.
     * @var bool
     */
    const QUANTITY_RESET                = true;

    /**
     * Gateway in use.
     * @var string
     */
    protected static $gatewayKey        = null;

    /**
     * Gateway instance.
     * @var object
     */
    protected static $gateway           = null;

    /**
     * Gatway in use.
     * @var string
     */
    protected static $exception         = null;

    /**
     * Laravel application
     *
     * @var \Illuminate\Foundation\Application
     */
    private $errorMessage;

    /**
     * Laravel application
     *
     * @var \Illuminate\Foundation\Application
     */
    public $app;

    /**
     * Create a new confide instance.
     *
     * @param \Illuminate\Foundation\Application $app
     *
     * @return void
     */
    public function __construct($app)
    {
        $this->app              = $app;
        static::$gatewayKey     = $this->getGateway();
        static::$exception      = null;
        static::$gateway        = static::instanceGateway();
    }

    /**
     * Get the currently authenticated user or null.
     *
     * @return Illuminate\Auth\UserInterface|null
     */
    public function user()
    {
        return $this->app->auth->user();
    }

    /**
     * Checkout current user's cart.
     */
    public static function setGateway($gatewayKey)
    {
        if (!array_key_exists($gatewayKey, Config::get('shop.gateways')))
            throw new ShopException('Invalid gateway.');
        static::$gatewayKey = $gatewayKey;
        static::$gateway    = static::instanceGateway();
        Session::push('shop.gateway', $gatewayKey);
    }

    /**
     * Checkout current user's cart.
     */
    public static function getGateway()
    {
        $gateways = Session::get('shop.gateway');
        return $gateways && count($gateways) > 0
            ? $gateways[count($gateways) - 1]
            : null;
    }

    /**
     * Checkout current user's cart.
     *
     * @param object $cart For specific cart.
     *
     * @return bool
     */
    public static function checkout($cart = null)
    {
        $success = true;
        try {
            if (empty(static::$gatewayKey)) {
                throw new ShopException('Payment gateway not selected.');
            }
            if (empty($cart)) $cart = Auth::user()->cart;
            static::$gateway->onCheckout($cart);
        } catch (ShopException $e) {
            static::setException($e);
            $success = false;
        } catch (CheckoutException $e) {
            static::$exception = $e;
            $success = false;
        } catch (GatewayException $e) {
            static::$exception = $e;
            $success = false;
        }
        if ($cart)
            \event(new CartCheckout($cart->id, $success));
        return $success;
    }

    /**
     * Returns placed order.
     *
     * @param object $cart For specific cart.
     *
     * @return object
     */
    public static function placeOrder($cart = null)
    {
        try {
            if (empty(static::$gatewayKey))
                throw new ShopException('Payment gateway not selected.');
            if (empty($cart)) $cart = Auth::user()->cart;
            $order = $cart->placeOrder();
            $statusCode = $order->statusCode;
            \event(new OrderPlaced($order->id));
            static::$gateway->setCallbacks($order);
            if (static::$gateway->onCharge($order)) {
                $order->statusCode = static::$gateway->getTransactionStatusCode();
                $order->save();
                // Create transaction
                $order->placeTransaction(
                    static::$gatewayKey,
                    static::$gateway->getTransactionId(),
                    static::$gateway->getTransactionDetail(),
                    static::$gateway->getTransactionToken()
                );
                // Fire event
                if ($order->isCompleted)
                    \event(new OrderCompleted($order->id));
            } else {
                $order->statusCode = 'failed';
                $order->save();
            }
        } catch (ShopException $e) {
            static::setException($e);
            if (isset($order)) {
                $order->statusCode = 'failed';
                $order->save();
                // Create failed transaction
                $order->placeTransaction(
                    static::$gatewayKey,
                    uniqid(),
                    static::$exception->getMessage(),
                    $order->statusCode
                );
            }
        } catch (GatewayException $e) {
            static::$exception = $e;
            if (isset($order)) {
                $order->statusCode = 'failed';
                $order->save();
                // Create failed transaction
                $order->placeTransaction(
                    static::$gatewayKey,
                    uniqid(),
                    static::$exception->getMessage(),
                    $order->statusCode
                );
            }
        }
        if ($order) {
            static::checkStatusChange($order, $statusCode);
            return $order;
        } else {
            return;
        }
    }

    /**
     * Handles gateway callbacks.
     *
     * @param string $order  Order.
     * @param string $status Callback status
     */
    public static function callback($order, $transaction, $status, $data = null)
    {
        $statusCode = $order->statusCode;
        try {
            if (in_array($status, ['success', 'fail'])) {
                static::$gatewayKey = $transaction->gateway;
                static::$gateway = static::instanceGateway();
                if ($status == 'success') {
                    static::$gateway->onCallbackSuccess($order, $data);
                    $order->statusCode = static::$gateway->getTransactionStatusCode();
                    // Create transaction
                    $order->placeTransaction(
                        static::$gatewayKey,
                        static::$gateway->getTransactionId(),
                        static::$gateway->getTransactionDetail(),
                        static::$gateway->getTransactionToken()
                    );
                    // Fire event
                    if ($order->isCompleted)
                        \event(new OrderCompleted($order->id));
                } else if ($status == 'fail') {
                    static::$gateway->onCallbackFail($order, $data);
                    $order->statusCode = 'failed';
                }
                $order->save();
            }
        } catch (ShopException $e) {
            static::setException($e);
            $order->statusCode = 'failed';
            $order->save();
        } catch (GatewayException $e) {
            static::setException($e);
            $order->statusCode = 'failed';
            $order->save();
        }
        static::checkStatusChange($order, $statusCode);
    }

    /**
     * Formats any value to price format set in config.
     *
     * @param mixed $value Value to format.
     *
     * @return string
     */
    public static function format($value)
    {
        return preg_replace(
            [
                '/:symbol/',
                '/:price/',
                '/:currency/'
            ],
            [
                Config::get('shop.currency_symbol'),
                $value,
                Config::get('shop.currency')
            ],
            Config::get('shop.display_price_format')
        );
    }

    /**
     * Retuns gateway.
     *
     * @return object
     */
    public static function gateway()
    {
        return static::$gateway;
    }

    /**
     * Retuns exception.
     *
     * @return Exception
     */
    public static function exception()
    {
        return static::$exception;
    }

    /**
     * Saves exception in class.
     *
     * @param mixed $e Exception
     */
    protected static function setException($e)
    {
        Log::error($e);
        static::$exception = $e;
    }

    /**
     * Retunes gateway object.
     * @return object
     */
    protected static function instanceGateway()
    {
        if (empty(static::$gatewayKey)) return;
        $className = '\\' . Config::get('shop.gateways')[static::$gatewayKey];
        return new $className(static::$gatewayKey);
    }

    /**
     * Check on order status differences and fires event.
     * @param object $order Order.
     * @param string $prevStatusCode Previous status code.
     * @return void
     */
    protected static function checkStatusChange($order, $prevStatusCode)
    {
        if (!empty($prevStatusCode) && $order->statusCode != $prevStatusCode)
            \event(new OrderStatusChanged($order->id, $order->statusCode, $prevStatusCode));
    }
}


<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateJobStatusesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('job_statuses', function (Blueprint $table) {
            $table->increments('id');
            $table->string('job_id')->index()->nullable();
            $table->string('type')->index();
            $table->string('queue')->index()->nullable();
            $table->integer('attempts')->default(0);
            $table->integer('progress_now')->default(0);
            $table->integer('progress_max')->default(0);
            $table->string('status', 16)->default(\Imtigger\LaravelJobStatus\JobStatus::STATUS_QUEUED)->index();
            $table->longText('input')->nullable();
            $table->longText('output')->nullable();
            $table->timestamps();
            $table->timestamp('started_at')->nullable();
            $table->timestamp('finished_at')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('job_statuses');
    }
}


<?php

namespace Flintstone\Formatter;

class SerializeFormatter implements FormatterInterface
{
    /**
     * {@inheritdoc}
     */
    public function encode($data): string
    {
        return serialize($this->preserveLines($data, false));
    }

    /**
     * {@inheritdoc}
     */
    public function decode(string $data)
    {
        return $this->preserveLines(unserialize($data), true);
    }

    /**
     * Preserve new lines, recursive function.
     *
     * @param mixed $data
     * @param bool $reverse
     *
     * @return mixed
     */
    protected function preserveLines($data, bool $reverse)
    {
        $search = ["\n", "\r"];
        $replace = ['\\n', '\\r'];

        if ($reverse) {
            $search = ['\\n', '\\r'];
            $replace = ["\n", "\r"];
        }

        if (is_string($data)) {
            $data = str_replace($search, $replace, $data);
        } elseif (is_array($data)) {
            foreach ($data as &$value) {
                $value = $this->preserveLines($value, $reverse);
            }
            unset($value);
        }

        return $data;
    }
}


<?php

namespace Flintstone\Formatter;

use Flintstone\Exception;

class JsonFormatter implements FormatterInterface
{
    /**
     * @var bool
     */
    private $assoc;

    public function __construct(bool $assoc = true)
    {
        $this->assoc = $assoc;
    }

    /**
     * {@inheritdoc}
     */
    public function encode($data): string
    {
        $result = json_encode($data);

        if (json_last_error() === JSON_ERROR_NONE) {
            return $result;
        }

        throw new Exception(json_last_error_msg());
    }

    /**
     * {@inheritdoc}
     */
    public function decode(string $data)
    {
        $result = json_decode($data, $this->assoc);

        if (json_last_error() === JSON_ERROR_NONE) {
            return $result;
        }

        throw new Exception(json_last_error_msg());
    }
}


<?php

namespace Flintstone;

use SplFileObject;
use SplTempFileObject;

class Database
{
    /**
     * File read flag.
     *
     * @var int
     */
    const FILE_READ = 1;

    /**
     * File write flag.
     *
     * @var int
     */
    const FILE_WRITE = 2;

    /**
     * File append flag.
     *
     * @var int
     */
    const FILE_APPEND = 3;

    /**
     * File access mode.
     *
     * @var array
     */
    protected $fileAccessMode = [
        self::FILE_READ => [
            'mode' => 'rb',
            'operation' => LOCK_SH,
        ],
        self::FILE_WRITE => [
            'mode' => 'wb',
            'operation' => LOCK_EX,
        ],
        self::FILE_APPEND => [
            'mode' => 'ab',
            'operation' => LOCK_EX,
        ],
    ];

    /**
     * Database name.
     *
     * @var string
     */
    protected $name;

    /**
     * Config class.
     *
     * @var Config
     */
    protected $config;

    /**
     * Constructor.
     *
     * @param string $name
     * @param Config|null $config
     */
    public function __construct(string $name, Config $config = null)
    {
        $this->setName($name);

        if ($config) {
            $this->setConfig($config);
        }
    }

    /**
     * Get the database name.
     *
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * Set the database name.
     *
     * @param string $name
     *
     * @throws Exception
     */
    public function setName(string $name)
    {
        Validation::validateDatabaseName($name);
        $this->name = $name;
    }

    /**
     * Get the config.
     *
     * @return Config
     */
    public function getConfig(): Config
    {
        return $this->config;
    }

    /**
     * Set the config.
     *
     * @param Config $config
     */
    public function setConfig(Config $config)
    {
        $this->config = $config;
    }

    /**
     * Get the path to the database file.
     *
     * @return string
     */
    public function getPath(): string
    {
        return $this->config->getDir() . $this->getName() . $this->config->getExt();
    }

    /**
     * Open the database file.
     *
     * @param int $mode
     *
     * @throws Exception
     *
     * @return SplFileObject
     */
    protected function openFile(int $mode): SplFileObject
    {
        $path = $this->getPath();

        if (!is_file($path) && !@touch($path)) {
            throw new Exception('Could not create file: ' . $path);
        }

        if (!is_readable($path) || !is_writable($path)) {
            throw new Exception('File does not have permission for read and write: ' . $path);
        }

        if ($this->getConfig()->useGzip()) {
            $path = 'compress.zlib://' . $path;
        }

        $res = $this->fileAccessMode[$mode];
        $file = new SplFileObject($path, $res['mode']);

        if ($mode === self::FILE_READ) {
            $file->setFlags(SplFileObject::DROP_NEW_LINE | SplFileObject::SKIP_EMPTY | SplFileObject::READ_AHEAD);
        }

        if (!$this->getConfig()->useGzip() && !$file->flock($res['operation'])) {
            $file = null;
            throw new Exception('Could not lock file: ' . $path);
        }

        return $file;
    }

    /**
     * Open a temporary file.
     *
     * @return SplTempFileObject
     */
    public function openTempFile(): SplTempFileObject
    {
        return new SplTempFileObject($this->getConfig()->getSwapMemoryLimit());
    }

    /**
     * Close the database file.
     *
     * @param SplFileObject $file
     *
     * @throws Exception
     */
    protected function closeFile(SplFileObject &$file)
    {
        if (!$this->getConfig()->useGzip() && !$file->flock(LOCK_UN)) {
            $file = null;
            throw new Exception('Could not unlock file');
        }

        $file = null;
    }

    /**
     * Read lines from the database file.
     *
     * @return \Generator
     */
    public function readFromFile(): \Generator
    {
        $file = $this->openFile(static::FILE_READ);

        try {
            foreach ($file as $line) {
                yield new Line($line);
            }
        } finally {
            $this->closeFile($file);
        }
    }

    /**
     * Append a line to the database file.
     *
     * @param string $line
     */
    public function appendToFile(string $line)
    {
        $file = $this->openFile(static::FILE_APPEND);
        $file->fwrite($line);
        $this->closeFile($file);
    }

    /**
     * Flush the database file.
     */
    public function flushFile()
    {
        $file = $this->openFile(static::FILE_WRITE);
        $this->closeFile($file);
    }

    /**
     * Write temporary file contents to database file.
     *
     * @param SplTempFileObject $tmpFile
     */
    public function writeTempToFile(SplTempFileObject &$tmpFile)
    {
        $file = $this->openFile(static::FILE_WRITE);

        foreach ($tmpFile as $line) {
            $file->fwrite($line);
        }

        $this->closeFile($file);
        $tmpFile = null;
    }
}


<?php

namespace Flintstone;

class Flintstone
{
    /**
     * Flintstone version.
     *
     * @var string
     */
    const VERSION = '2.2';

    /**
     * Database class.
     *
     * @var Database
     */
    protected $database;

    /**
     * Config class.
     *
     * @var Config
     */
    protected $config;

    /**
     * Constructor.
     *
     * @param Database|string $database
     * @param Config|array $config
     */
    public function __construct($database, $config)
    {
        if (is_string($database)) {
            $database = new Database($database);
        }

        if (is_array($config)) {
            $config = new Config($config);
        }

        $this->setDatabase($database);
        $this->setConfig($config);
    }

    /**
     * Get the database.
     *
     * @return Database
     */
    public function getDatabase(): Database
    {
        return $this->database;
    }

    /**
     * Set the database.
     *
     * @param Database $database
     */
    public function setDatabase(Database $database)
    {
        $this->database = $database;
    }

    /**
     * Get the config.
     *
     * @return Config
     */
    public function getConfig(): Config
    {
        return $this->config;
    }

    /**
     * Set the config.
     *
     * @param Config $config
     */
    public function setConfig(Config $config)
    {
        $this->config = $config;
        $this->getDatabase()->setConfig($config);
    }

    /**
     * Get a key from the database.
     *
     * @param string $key
     *
     * @return mixed
     */
    public function get(string $key)
    {
        Validation::validateKey($key);

        // Fetch the key from cache
        if ($cache = $this->getConfig()->getCache()) {
            if ($cache->contains($key)) {
                return $cache->get($key);
            }
        }

        // Fetch the key from database
        $file = $this->getDatabase()->readFromFile();
        $data = false;

        foreach ($file as $line) {
            /** @var Line $line */
            if ($line->getKey() == $key) {
                $data = $this->decodeData($line->getData());
                break;
            }
        }

        // Save the data to cache
        if ($cache && $data !== false) {
            $cache->set($key, $data);
        }

        return $data;
    }

    /**
     * Set a key in the database.
     *
     * @param string $key
     * @param mixed $data
     */
    public function set(string $key, $data)
    {
        Validation::validateKey($key);

        // If the key already exists we need to replace it
        if ($this->get($key) !== false) {
            $this->replace($key, $data);
            return;
        }

        // Write the key to the database
        $this->getDatabase()->appendToFile($this->getLineString($key, $data));

        // Delete the key from cache
        if ($cache = $this->getConfig()->getCache()) {
            $cache->delete($key);
        }
    }

    /**
     * Delete a key from the database.
     *
     * @param string $key
     */
    public function delete(string $key)
    {
        Validation::validateKey($key);

        if ($this->get($key) !== false) {
            $this->replace($key, false);
        }
    }

    /**
     * Flush the database.
     */
    public function flush()
    {
        $this->getDatabase()->flushFile();

        // Flush the cache
        if ($cache = $this->getConfig()->getCache()) {
            $cache->flush();
        }
    }

    /**
     * Get all keys from the database.
     *
     * @return array
     */
    public function getKeys(): array
    {
        $keys = [];
        $file = $this->getDatabase()->readFromFile();

        foreach ($file as $line) {
            /** @var Line $line */
            $keys[] = $line->getKey();
        }

        return $keys;
    }

    /**
     * Get all data from the database.
     *
     * @return array
     */
    public function getAll(): array
    {
        $data = [];
        $file = $this->getDatabase()->readFromFile();

        foreach ($file as $line) {
            /** @var Line $line */
            $data[$line->getKey()] = $this->decodeData($line->getData());
        }

        return $data;
    }

    /**
     * Replace a key in the database.
     *
     * @param string $key
     * @param mixed $data
     */
    protected function replace(string $key, $data)
    {
        // Write a new database to a temporary file
        $tmpFile = $this->getDatabase()->openTempFile();
        $file = $this->getDatabase()->readFromFile();

        foreach ($file as $line) {
            /** @var Line $line */
            if ($line->getKey() == $key) {
                if ($data !== false) {
                    $tmpFile->fwrite($this->getLineString($key, $data));
                }
            } else {
                $tmpFile->fwrite($line->getLine() . "\n");
            }
        }

        $tmpFile->rewind();

        // Overwrite the database with the temporary file
        $this->getDatabase()->writeTempToFile($tmpFile);

        // Delete the key from cache
        if ($cache = $this->getConfig()->getCache()) {
            $cache->delete($key);
        }
    }

    /**
     * Get the line string to write.
     *
     * @param string $key
     * @param mixed $data
     *
     * @return string
     */
    protected function getLineString(string $key, $data): string
    {
        return $key . '=' . $this->encodeData($data) . "\n";
    }

    /**
     * Decode a string into data.
     *
     * @param string $data
     *
     * @return mixed
     */
    protected function decodeData(string $data)
    {
        return $this->getConfig()->getFormatter()->decode($data);
    }

    /**
     * Encode data into a string.
     *
     * @param mixed $data
     *
     * @return string
     */
    protected function encodeData($data): string
    {
        return $this->getConfig()->getFormatter()->encode($data);
    }
}